<template>
  <transition
    :name="props.transition"
    :enter-active-class="props.enterActiveClass"
    :leave-active-class="props.leaveActiveClass"
    @after-leave="props.afterLeaveDestroy && doDestroy()"
  >
    <div
      ref="popperRef"
      class="vc-popper"
      :class="props.popperClass"
      :style="{ ...popperStyle, ...floatingStyles }"
      :data-popper-placement="floatingStyles.placement"
      v-if="!props.disabled && visible"
      :aria-hidden="props.disabled || !visible ? 'true' : 'false'"
      :tabindex="props.tabindex"
      role="popper"
    >
      <div class="vc-popper-arrow" v-if="props.showArrow" ref="arrowRef" :style="arrowStyle"></div>
      <slot>{{ props.content }}</slot>
    </div>
  </transition>
  <vc-popper-wrapper ref="referenceRef" v-if="$slots?.reference">
    <template #default>
      <slot name="reference"></slot>
    </template>
  </vc-popper-wrapper>
</template>

<script>
  export default {
    name: 'VcPopper',
  };
</script>

<script setup>
  import {
    computed,
    onBeforeUnmount,
    onMounted,
    onUnmounted,
    watch,
    ref,
    useSlots,
    reactive,
    watchEffect,
    getCurrentInstance,
  } from 'vue';
  import {
    arrow,
    autoPlacement,
    autoUpdate,
    computePosition,
    detectOverflow,
    flip,
    getOverflowAncestors,
    hide,
    inline,
    limitShift,
    offset,
    platform,
    shift,
    size,
    useFloating,
  } from '@floating-ui/vue';
  import useZIndex from '@/hooks/useZIndex.js';
  import { on, off, getType, elementContains } from '@/utils/config.js';
  import createSlot from './createSlot';
  const { setIndex, getIndex } = useZIndex('popper');

  const referenceSlot = useSlots()?.reference?.()?.[0];

  const VcPopperWrapper = referenceSlot && createSlot(referenceSlot);

  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: undefined,
    },
    // 弹框位置
    placement: {
      default: 'top',
      type: String,
      validator: (value) =>
        [
          'top',
          'top-start',
          'top-end',
          'bottom',
          'bottom-start',
          'bottom-end',
          'left',
          'left-start',
          'left-end',
          'right',
          'right-start',
          'right-end',
        ].indexOf(value) > -1,
    },
    // 定位方式
    strategy: {
      default: 'absolute',
      type: String,
      validator: (value) => ['absolute', 'fixed'].indexOf(value) > -1,
    },
    // 触发方式
    trigger: {
      type: String,
      default: 'click',
      validator: (value) => ['click', 'toggle', 'hover', 'focus', 'manual'].indexOf(value) > -1,
    },
    // 鼠标移入延迟
    delayOnMouseOver: {
      type: Number,
      default: 10,
    },
    // 鼠标移出延迟
    delayOnMouseOut: {
      type: Number,
      default: 10,
    },
    // 禁选
    disabled: {
      type: Boolean,
      default: false,
    },
    // popper 内容主体
    content: String,
    // 过渡：移入样式
    enterActiveClass: String,
    // 过渡：移出样式
    leaveActiveClass: String,
    // 离开后销毁popper
    afterLeaveDestroy: {
      type: Boolean,
      default: false,
    },
    // 边界元素
    boundaryEle: [HTMLElement, String, Object],
    // reference 主体元素
    reference: [HTMLElement, String, Object],
    virtualRef: undefined,
    // 强制显示弹框
    forceShow: {
      type: Boolean,
      default: false,
    },
    // 挂载到body下
    appendToBody: {
      type: Boolean,
      default: false,
    },
    // 启用箭头
    showArrow: {
      type: Boolean,
      default: true,
    },
    // 过渡动画样式
    transition: {
      type: String,
      default: '',
    },
    // 中断冒泡事件
    stopPropagation: {
      type: Boolean,
      default: false,
    },
    // 阻止默认事件
    preventDefault: {
      type: Boolean,
      default: false,
    },
    // 配置项
    options: {
      type: Object,
      default() {
        return {};
      },
    },
    // popper 根元素样式
    popperClass: {
      type: String,
      default: '',
    },
    // popper 根元素样式
    popperStyle: {
      type: Object,
      default: () => {},
    },
    tabindex: {
      type: Number,
      default: -1,
    },
    offset: {
      type: Number,
      default: 12,
    },
  });

  const emits = defineEmits(['update:modelValue', 'show', 'hide']);

  const popperRef = ref(null);
  const referenceRef = ref(null);
  const arrowRef = ref(null);

  const referenceElm = ref(null);

  const floatingRef = ref(null);
  /// options 配置项
  const popperOptions = ref({});

  const popperStyle = ref();
  const floatingStyles = ref({});
  const arrowStyle = ref({});

  const visible = ref(false);
  // 是否已挂载到body上
  const appendedToBody = ref(false);

  if (props.trigger === 'manual') {
    watchEffect(() => {
      visible.value = props.modelValue;
    });
    watch(visible, (newValue) => {
      emits('update:modelValue', newValue);
    });
  }

  onMounted(() => {
    setIndex('popper');
    popperStyle.value = { zIndex: getIndex('popper'), ...props.popperStyle }
    watchEffect(() => {
      if (props.reference) {
        if (getType(props.reference) === 'string') {
          referenceElm.value = document.querySelector(props.reference);
        } else {
          referenceElm.value = props.reference?.$el || props.reference;
        }
      } else if (props.virtualRef) {
        referenceElm.value = props.virtualRef;
        visible.value = !visible.value;
      } else {
        referenceElm.value = referenceRef.value?.slotRef?.$el;
      }
      if (referenceElm.value && !props.virtualRef && props.trigger !== 'manual') {
        handleControl();
      }
    });
    watchEffect(() => {
      if (popperRef.value && visible.value) {
        firstUpdate();
      }
    });
  });

  const setBoundary = () => {
    // 获取边界元素
    const boundariesElement =
      getType(props.boundaryEle) === 'object'
        ? props.boundaryEle?.$el
        : getType(props.boundaryEle) === 'element'
        ? props.boundaryEle
        : document.querySelector(props.boundaryEle);
    Object.assign(popperOptions.value, {
      platform: {
        ...platform,
        getClippingRect: () => boundariesElement.getBoundingClientRect(),
      },
    });
  };

  const handleControl = () => {
    switch (props.trigger) {
      case 'click':
        on(referenceElm.value, 'click', showPopper);
        on(document, 'click', handleClick);
        break;
      case 'toggle':
        on(referenceElm.value, 'click', togglePopper);
        break;
      case 'hover':
        on(referenceElm.value, 'mouseover', onMouseOver);
        on(referenceElm.value, 'mouseout', onMouseOut);
        break;
      case 'focus':
        on(referenceElm.value, 'focus', showPopper);
        on(referenceElm.value, 'blur', hidePopper);
        break;
    }
  };

  const onCreatePopper = () => {
    if (props.trigger === 'hover' && !props.virtualRef) {
      on(popperRef.value, 'mouseover', onMouseOver);
      on(popperRef.value, 'mouseout', onMouseOut);
    }
    floatingRef.value = useFloating(referenceElm.value, popperRef.value, {
      ...popperOptions.value,
      whileElementsMounted: cleanup(),
    });
    // 挂载到body下
    if (props.appendToBody && !appendedToBody.value) {
      appendedToBody.value = true;
      document.body.appendChild(popperRef.value);
    }
  };

  const cleanup = () => autoUpdate(referenceElm.value, popperRef.value, updatePopper);

  const updatePopper = () => {
    visible.value &&
      computePosition(referenceElm.value, popperRef.value, popperOptions.value).then(
        ({ x, y, middlewareData, placement, strategy }) => {
          floatingStyles.value = { left: x + 'px', top: y + 'px', position: strategy, placement };
          if (props.showArrow) {
            const { x: arrowX, y: arrowY } = middlewareData.arrow;
            const staticSide = {
              top: 'bottom',
              right: 'left',
              bottom: 'top',
              left: 'right',
            }[placement.split('-')[0]];
            Object.assign(arrowStyle.value, {
              left: arrowX != null ? `${arrowX}px` : '',
              top: arrowY != null ? `${arrowY}px` : '',
              [staticSide]: '-8px',
              zIndex: -1,
            });
          }
        }
      );
  };

  const firstUpdate = () => {
    const defaultMiddleware = [
      offset(props.offset),
      flip(),
      shift(),
      props.showArrow && arrow({ element: arrowRef.value }),
    ];
    // 接收并合并传入的配置项
    popperOptions.value.placement = props.placement;
    if (props.strategy) {
      popperOptions.value.strategy = props.strategy;
    }
    if (props.boundaryEle) {
      setBoundary();
    }
    Object.assign(popperOptions.value, { middleware: defaultMiddleware,...props.options?.middleware }, props.options);
    onCreatePopper();
  };

  // 开启弹框
  const showPopper = () => {
    visible.value = true;
    emits('show');
  };
  // 关闭弹框
  const hidePopper = () => {
    visible.value = false;
    emits('hide');
  };

  // 点击开关弹框
  const togglePopper = (event) => {
    if (props.stopPropagation) {
      event.stopPropagation();
    }
    if (props.preventDefault) {
      event.preventDefault();
    }
    if(visible.value){
      if(!props.forceShow){
        hidePopper()
      }
    } else {
      showPopper()
    }
  };

  // 销毁弹框
  const doDestroy = () => {
    // 如果已打开
    if (visible.value) {
      return;
    }
    // 如果已创建 则把节点销毁并置为null
    if (floatingRef.value) {
      floatingRef.value = null;
    }
    // 如果已挂载到body下则将已挂载body完成置为false并移出body下此弹框
    if (appendedToBody.value) {
      appendedToBody.value = false;
      popperRef.value && document.body.removeChild(popperRef.value);
    }
  };

  // 销毁popper及监听
  const destroyPopper = () => {
    if (!props.virtualRef && props.trigger !== 'manual') {
      props.trigger === 'toggle' && off(referenceElm.value, 'click', togglePopper);
      props.trigger === 'click' && off(referenceElm.value, 'click', showPopper);
      off(referenceElm.value, 'focus', showPopper);
      off(referenceElm.value, 'blur', hidePopper);
      off(referenceElm.value, 'mouseout', onMouseOut);
      off(referenceElm.value, 'mouseover', onMouseOver);
      off(document, 'click', handleClick);
      if (props.trigger === 'hover' && popperRef.value) {
        off(popperRef.value, 'mouseover', onMouseOver);
        off(popperRef.value, 'mouseout', onMouseOut);
      }
    }

    hidePopper();
    doDestroy();
  };

  const handleClick = (e) => {
    if (
      !referenceElm.value ||
      elementContains(referenceElm.value, e.target) ||
      !popperRef.value ||
      elementContains(popperRef.value, e.target)
    ) {
      return;
    }
    if (props.forceShow) {
      return;
    }

    hidePopper();
  };

  const openDelay = ref(false);
  const closeDelay = ref(false);
  let timer = ref(null);
  // 关闭定时器
  const clearTimer = () => {
    if (openDelay.value || closeDelay.value) {
      clearTimeout(timer);
    }
  };

  // 鼠标移入延时
  const onMouseOver = () => {
    clearTimeout(timer);
    if (props.delayOnMouseOver) {
      timer = setTimeout(() => {
        showPopper();
      }, props.delayOnMouseOver);
    } else {
      showPopper();
    }
  };
  // 鼠标移出延时
  const onMouseOut = () => {
    clearTimeout(timer);
    if (props.delayOnMouseOut) {
      timer = setTimeout(() => {
        hidePopper();
      }, props.delayOnMouseOut);
    } else {
      hidePopper();
    }
  };

  onDeactivated(() => {
    clearTimer();
  });

  onBeforeUnmount(() => {
    clearTimer();
  });

  onUnmounted(() => {
    destroyPopper();
  });

  defineExpose({
    show:showPopper,
    hide:hidePopper,
    toggle:togglePopper
  })
</script>
<style lang="less">
  @arrowWidth: 8px;
  @arrowOffset: -8px;
  div[role='tooltip'] {
    display: inline-block;
  }
  .vc-popper {
    position: absolute;
    width: auto;
    box-sizing: content-box;
    padding: 18px 20px;
    font-weight: normal;
    background: @white;
    border-radius: 4px;
    border: 1px solid @gray-eb;
    color: #606266;
    line-height: 1.4;
    text-align: justify;
    font-size: 14px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 10%);
    word-break: break-all;
    &-arrow {
      width: 0;
      height: 0;
      border-style: solid;
      border-width: @arrowWidth;
      border-color: transparent;
      position: absolute;
      display: block;
      margin: 0;
      filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
      &:after {
        content: '';
        border-width: @arrowWidth;
        position: absolute;
        display: block;
        width: 0;
        height: 0;
        border-color: transparent;
        border-style: solid;
      }
    }
  }

  .vc-popper[data-popper-placement^='top'] {
    .vc-popper-arrow {
      border-bottom-width: 0;
      border-top-color: @gray-eb;
      &:after {
        bottom: 1px;
        border-bottom-width: 0;
        border-top-color: @white;
        margin-left: @arrowOffset;
      }
    }
  }

  .vc-popper[data-popper-placement^='bottom'] {
    .vc-popper-arrow {
      border-top-width: 0;
      border-bottom-color: @gray-eb;
      &:after {
        top: 1px;
        margin-left: @arrowOffset;
        border-top-width: 0;
        border-bottom-color: @white;
      }
    }
  }

  .vc-popper[data-popper-placement^='right'] {
    .vc-popper-arrow {
      border-left-width: 0;
      border-right-color: @gray-eb;
      &:after {
        left: 1px;
        margin-top: @arrowOffset;
        border-left-width: 0;
        border-right-color: @white;
      }
    }
  }

  .vc-popper[data-popper-placement^='left'] {
    .vc-popper-arrow {
      border-right-width: 0;
      border-left-color: @gray-eb;
      &:after {
        right: 1px;
        margin-top: @arrowOffset;
        border-right-width: 0;
        border-left-color: @white;
      }
    }
  }
</style>
